-- -------------------------------------------------------------------------------------------------
-- Copyright (c) Lukas Vik. All rights reserved.
--
-- This file is part of the hdl-modules project, a collection of reusable, high-quality,
-- peer-reviewed VHDL building blocks.
-- https://hdl-modules.com
-- https://github.com/hdl-modules/hdl-modules
-- -------------------------------------------------------------------------------------------------
-- Generic, parameterizable, register file for AXI-Lite register buses.
-- Is parameterizable via a generic that sets the list of registers, with their modes and their
-- default values.
--
-- Will respond with ``SLVERR`` on the ``R`` channel when attempting to read a register that
--
-- 1. Does not exists (``ARADDR`` out of range), or
-- 2. Is not of a register type that can be read by the bus (e.g. write only).
--
-- Similarly, it will respond with ``SLVERR`` on the ``B`` channel when attempting to write a
-- register that
--
-- 1. Does not exists (``AWADDR`` out of range), or
-- 2. Is not of a register type that can be written by the bus (e.g. read only).
--
-- Both cases are handled cleanly without stalling or hanging the AXI-Lite bus.
--
-- The ``regs`` and ``default_values`` generics are designed to get their values
-- from a package generated by the ``hdl-registers`` VHDL generator:
-- :py:class:`VhdlRegisterPackageGenerator
-- <hdl_registers.generator.vhdl.register_package.VhdlRegisterPackageGenerator>`.
-- The values can be constructed by hand as well, of course.
-- -------------------------------------------------------------------------------------------------

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

library axi;
use axi.axi_pkg.all;

library axi_lite;
use axi_lite.axi_lite_pkg.all;

library math;
use math.math_pkg.all;

use work.register_file_pkg.all;


entity axi_lite_register_file is
  generic (
    registers : register_definition_vec_t;
    default_values : register_vec_t(registers'range) := (others => (others => '0'))
  );
  port (
    clk : in std_ulogic;
    --# {{}}
    --# Register control bus
    axi_lite_m2s : in axi_lite_m2s_t;
    axi_lite_s2m : out axi_lite_s2m_t := axi_lite_s2m_init;
    --# {{}}
    -- Register values
    regs_up : in register_vec_t(registers'range) := default_values;
    regs_down : out register_vec_t(registers'range) := default_values;
    --# {{}}
    -- Each bit is pulsed for one cycle when the corresponding register is read/written.
    -- For read, the bit is asserted the exact same cycle as the AXI-Lite R transaction occurs.
    -- For write, the bit is asserted the cycle after the AXI-Lite W transaction occurs, so that
    -- 'regs_down' is updated with the new value.
    reg_was_read : out std_ulogic_vector(registers'range) := (others => '0');
    reg_was_written : out std_ulogic_vector(registers'range) := (others => '0')
  );
end entity;

architecture a of axi_lite_register_file is

  constant num_address_bits : positive := num_bits_needed(get_highest_index(registers));
  subtype address_range is natural range num_address_bits + 2 - 1 downto 2;

  signal reg_values : register_vec_t(registers'range) := default_values;

begin

  ------------------------------------------------------------------------------
  assign_down : process(all)
  begin
    -- Assign only the bits that are marked as utilized, so there is no risk of confusion/misuse.
    for reg_idx in registers'range loop
      if is_write_mode(registers(reg_idx).mode) then
        regs_down(reg_idx)(registers(reg_idx).utilized_width - 1 downto 0) <= reg_values(reg_idx)(
          registers(reg_idx).utilized_width - 1 downto 0
        );
      end if;
    end loop;
  end process;


  ------------------------------------------------------------------------------
  read_block : block
    type read_state_t is (ar, r);
    signal read_state : read_state_t := ar;
    signal read_index : u_unsigned(num_address_bits - 1 downto 0) := (others => '0');
  begin

    ------------------------------------------------------------------------------
    set_status : process(all)
    begin
      reg_was_read <= (others => '0');

      axi_lite_s2m.read.r.resp <= axi_resp_slverr;

      -- Set initial values zero.
      -- Below we will only assign the bits that are actually utilized by the register.
      -- Hopefully, software should not look at any bits outside of the utilized width,
      -- but set zero just in case and to avoid confusion.
      -- Does not impact netlist build size.
      axi_lite_s2m.read.r.data(reg_values(0)'range) <= (others => '0');

      for list_idx in registers'range loop
        if is_read_mode(registers(list_idx).mode) then
          if read_index = list_idx then
            axi_lite_s2m.read.r.resp <= axi_resp_okay;
            reg_was_read(list_idx) <= axi_lite_m2s.read.r.ready and axi_lite_s2m.read.r.valid;
          end if;

          for bit_idx in 0 to registers(list_idx).utilized_width - 1 loop
            if is_application_gives_value_mode(registers(list_idx).mode) then
              if read_index = list_idx then
                axi_lite_s2m.read.r.data(bit_idx) <= regs_up(list_idx)(bit_idx);
              end if;
            else
              if read_index = list_idx then
                axi_lite_s2m.read.r.data(bit_idx) <= reg_values(list_idx)(bit_idx);
              end if;
            end if;
          end loop;
        end if;
      end loop;
    end process;


    ------------------------------------------------------------------------------
    read_process : process
    begin
      wait until rising_edge(clk);

      -- Default assignments.
      axi_lite_s2m.read.ar.ready <= '0';
      axi_lite_s2m.read.r.valid <= '0';

      case read_state is
        when ar =>
          -- Sample always, will only be used if we change state.
          read_index <= axi_lite_m2s.read.ar.addr(address_range);

          -- Keep high and then lower it after we have a 'valid'.
          -- Will be high at one rising clock edge together with 'valid'.
          -- Note that we will spend at least one clock cycle in the 'r' state,
          -- so we can look at 'valid' straight away when we return to this state, knowing that
          -- the previous AR word has been popped.
          axi_lite_s2m.read.ar.ready <= '1';

          if axi_lite_m2s.read.ar.valid then
            read_state <= r;
          end if;

        when r =>
          axi_lite_s2m.read.r.valid <= '1';

          if axi_lite_m2s.read.r.ready and axi_lite_s2m.read.r.valid then
            axi_lite_s2m.read.r.valid <= '0';

            read_state <= ar;
          end if;
      end case;
    end process;
  end block;


  ------------------------------------------------------------------------------
  write_block : block
    type write_state_t is (aw, w, b);
    signal write_state : write_state_t := aw;
    signal write_index : u_unsigned(num_address_bits - 1 downto 0) := (others => '0');
  begin


    ------------------------------------------------------------------------------
    set_status : process
    begin
      wait until rising_edge(clk);

      reg_was_written <= (others => '0');

      axi_lite_s2m.write.b.resp <= axi_resp_slverr;

      for list_idx in registers'range loop
        if is_write_pulse_mode(registers(list_idx).mode) then
          for bit_idx in 0 to registers(list_idx).utilized_width - 1 loop
            -- Set initial default value.
            -- If a write occurs to this register, the value will be asserted for one cycle below.
            reg_values(list_idx)(bit_idx) <= default_values(list_idx)(bit_idx);
          end loop;
        end if;
      end loop;

      for list_idx in registers'range loop
        if is_write_mode(registers(list_idx).mode) then
          if write_index = list_idx then
            axi_lite_s2m.write.b.resp <= axi_resp_okay;
            reg_was_written(list_idx) <= axi_lite_s2m.write.w.ready and axi_lite_m2s.write.w.valid;
          end if;

          for bit_idx in 0 to registers(list_idx).utilized_width - 1 loop
            if write_index = list_idx then
              if axi_lite_s2m.write.w.ready and axi_lite_m2s.write.w.valid then
                reg_values(list_idx)(bit_idx) <= axi_lite_m2s.write.w.data(bit_idx);
              end if;
            end if;
          end loop;
        end if;
      end loop;
    end process;


    ------------------------------------------------------------------------------
    -- The handshaking is not really optimal, but if it is "optimized" the LUT usage increases
    -- by 100% for some reason.
    -- Leave as it is for now, might be worth investigating at some point in the future.
    write_process : process
    begin
      wait until rising_edge(clk);

      case write_state is
        when aw =>
          -- Sample always, will only be used if we change state.
          write_index <= axi_lite_m2s.write.aw.addr(address_range);

          axi_lite_s2m.write.aw.ready <= '1';

          if axi_lite_m2s.write.aw.valid and axi_lite_s2m.write.aw.ready then
            axi_lite_s2m.write.aw.ready <= '0';
            axi_lite_s2m.write.w.ready <= '1';

            write_state <= w;
          end if;

        when w =>
          if axi_lite_m2s.write.w.valid and axi_lite_s2m.write.w.ready then
            axi_lite_s2m.write.w.ready <= '0';
            axi_lite_s2m.write.b.valid <= '1';

            write_state <= b;
          end if;

        when b =>
          if axi_lite_m2s.write.b.ready and axi_lite_s2m.write.b.valid then
            axi_lite_s2m.write.aw.ready <= '1';
            axi_lite_s2m.write.b.valid <= '0';

            write_state <= aw;
          end if;
      end case;
    end process;
  end block;

end architecture;
